package fr.bludwarf.skywarrior.ile;

import java.util.ArrayDeque;
import java.util.Collection;
import java.util.HashMap;
import java.util.Queue;
import java.util.Vector;

import fr.bludwarf.skywarrior.evenements.CommencerConstruction;
import fr.bludwarf.skywarrior.evenements.Evenement;
import fr.bludwarf.skywarrior.evenements.NouvelHabitant;
import fr.bludwarf.skywarrior.evenements.TerminerConstruction;
import fr.bludwarf.skywarrior.exceptions.ConstructionImpossible;
import fr.bludwarf.skywarrior.exceptions.EvenementInconnuOublie;
import fr.bludwarf.skywarrior.exceptions.RessourcesInsuffisantes;
import fr.bludwarf.skywarrior.global.Etat;
import fr.bludwarf.skywarrior.global.Temps;
import fr.bludwarf.skywarrior.global.TimeFactorInconnu;
import fr.bludwarf.skywarrior.joueur.EtatJoueur;
import fr.bludwarf.skywarrior.joueur.Ressources;
import fr.bludwarf.skywarrior.joueur.TypeUnite;
import fr.bludwarf.skywarrior.joueur.TypeVaisseau;

public class EtatIle extends Etat<Ile> {
	
	/** Population initiale sur l'ile principale */
	public static final int POP_INITIALE = 3;

	/** Nombre de bâtiment de chaque type */
	HashMap<TypeBatiment, Integer> _batiments = new HashMap<TypeBatiment, Integer>();
	
	/** File d'attente des construction, les temps restants ne prenne en compte que l'état actuel de l'ile */
	Queue<Construction> _constructions = new ArrayDeque<Construction>();
	
	/** Population */
	int _pop;
	
	/** Nourriture */
	int _nourriture;
	
	/** Temps restant avant la prochaine pop */
	double _nextPop;
	
	public EtatIle(final Ile ile, final EtatJoueur etatJoueur) {
		super(ile, etatJoueur);
		
		if (ile.ilePrincipale())
		{
			// De base, l'ile principale possède un palais
			_batiments.put(TypeBatiment.PALAIS, 1);
			_pop = POP_INITIALE;
		}
		else
		{
			_pop = 0;
		}
		
		_nourriture = getProduction(true).nourriture();
		_nextPop = getInitialNextPop();
	}

	public EtatIle(EtatIle etatInitial, final EtatJoueur etatJoueur) {
		super(etatInitial, etatJoueur);
		
		// Clone des bâtiments
		for (final TypeBatiment batiment : etatInitial._batiments.keySet())
		{
			final Integer nb = etatInitial._batiments.get(batiment);
			_batiments.put(batiment, nb);
		}
		
		// Clone des constructions
		for (final Construction construction : etatInitial._constructions)
		{
			final Construction newConstruction = new Construction(construction, this);
			_constructions.add(newConstruction);
		}
		
		_pop = etatInitial._pop;
		_nourriture = etatInitial._nourriture;
		_nextPop = etatInitial._nextPop;
	}

	/**
	 * Continue ou commence la prochaine construction pendant un certain temps. Ne termine pas la construction !
	 * @param temps temps passer à la construction
	 * @throws ConstructionImpossible si le bâtiment n'a pas été commencé
	 * @throws EvenementInconnuOublie 
	 */
	private void construire(final double temps) throws ConstructionImpossible, EvenementInconnuOublie {
		final Construction constructionEnCours = _constructions.peek();
		
		if (constructionEnCours != null && constructionEnCours.aEteCommencee())
		{
			constructionEnCours.construire(temps);
		}
	}
	
	/**
	 * @return la production totale de matériaux et d'éther de l'île
	 */
	public Ressources getProduction(final boolean cumulNourriture)
	{
		Ressources prod = new Ressources(0, 0, 0);
		
		for (final TypeBatiment batiment : _batiments.keySet())
		{
			prod.add(Ressources.total(getNb(batiment), getProduction(batiment, this)), cumulNourriture);
		}		
		
		return prod;
	}

	/**
	 * @param batiment type du bâtiment recherché
	 * @return le nombre de bâtiments (terminés) du type recherché dans cette île
	 */
	public int getNb(TypeBatiment batiment) {
		if (_batiments.containsKey(batiment))
		{
			return _batiments.get(batiment);
		}
		else
		{
			return 0;
		}
	}

	/**
	 * @param batiment type du bâtiment recherché
	 * @param etatIle 
	 * @return production d'un type de bâtiment à un instant donné sur cette île
	 */
	public Ressources getProduction(TypeBatiment batiment, EtatIle etatIle) {
		return batiment.getProduction(etatIle);
	}
	
	public StatsCombat getStatsCombat()
	{
		final StatsCombat stats = new StatsCombat(0, 1, 1); // de base on a 1 d'att et 1 de def
		for (final TypeBatiment bat : _batiments.keySet())
		{
			final int nb = _batiments.get(bat);
			stats.add(bat.getStatsCombat(), nb);
		}
		return stats;
	}
	
	/**
	 * Ajoute un nouveau bâtiment dans la liste des bâtiments à construire
	 * @param batiment type de bâtiment à construire
	 * @throws TimeFactorInconnu si le TimeFactor est inconnu pour le type demandé
	 */
	public void construire(TypeUnite unite) throws TimeFactorInconnu
	{
		_constructions.add(new Construction(this, unite));
	}

	/**
	 * @return file d'attente des construction, les temps restants ne prenne en compte que <b>l'état actuel</b> de l'ile
	 */
	public Queue<Construction> getConstructions() {
		return _constructions;
	}
	
	public void incPop(final int nb) throws TimeFactorInconnu
	{
		setPop(pop() + nb);
	}
	
	public void decPop(final int nb) throws TimeFactorInconnu
	{
		setPop(Math.max(0, pop() - nb));
	}
	
	@Override
	// FIXME : créer un constructeur de recopie
	public String toString() {
		final StringBuilder sb = new StringBuilder();

		final StatsCombat statsCombat = getStatsCombat();
		final int att = statsCombat.attaque();
		final int def = statsCombat.defense();
		sb.append(_entite.getNom() + "[").append(_pop).append("/").append(nourriture()) // pop/nourriture
			.append("(+:").append(Temps.toString(_nextPop)).append(");")
			.append(att).append("/").append(def).append("] : ") // att/def
			.append(_batiments).append(" : ") // batiment construits
			.append(_constructions); // batiment en cours

		return sb.toString();
	}

	@Override
	public Evenement prochainEvenement() {
		
		final Construction prchConst = _constructions.peek();		
		
		// Evenements
		final Collection<Evenement> evenements = new Vector<Evenement>();
		
		// prochain habitant
		evenements.add(new NouvelHabitant(this, _nextPop));

		if (prchConst != null)
		{
			// Si la prochaine construction n'a pas été comencée
			// alors le prochain évènement sur les constructions sera une DEBUT de construction
			if (!prchConst.aEteCommencee())
			{
				evenements.add(new CommencerConstruction(getEtatJoueur(), prchConst));
			}
			
			// Sinon le prochaine évènement sera un FIN de construction
			else
			{
				evenements.add(new TerminerConstruction(this, prchConst));
			}
		}
		
		return Evenement.prochainEvenementParmis(evenements);
	}

	public EtatJoueur getEtatJoueur() {
		return (EtatJoueur) _etatPere;
	}

	public void execute(CommencerConstruction evenement) throws ConstructionImpossible, TimeFactorInconnu {
		final Construction construction = evenement.getConstruction();
		
		// Consommation de ressource joueur
		try
		{
			getEtatJoueur().consommer(construction.getCout());
		}
		catch (final RessourcesInsuffisantes e)
		{
			throw new ConstructionImpossible("Impossible de construire " + construction.getUnite(), e);
		}
		
		// Conso de pop pour l'ile
		decPop(construction.getCoutPop());
		
		// Si on a pu consommer les ressources, on commence la construction
		construction.commencer();
	}

	public TypeUnite execute(final TerminerConstruction evenement) {
		final TypeUnite unite = _constructions.poll()._unite;
		
		add(unite);
		
		// Recalcul de la nourriture
		nourriture(getProduction(true).nourriture());
		
		return unite;
	}

	/**
	 * Calcul avec getTimeFactor
	 * @param nourriture
	 * @return facteur de modification des temps de construction (voir {@link #getTimeFactor(double)})
	 */
	public void nourriture(final int nourriture) {
		
		// Si la nourriture a changé, on doit changer le temps restant après le nextPop
		if (_nourriture != nourriture)
		{			
			_nextPop = Temps.applyTimeFactor(Temps.getTimeFactor(_nourriture, nourriture, _pop), _nextPop);
			
			// On modifie la nourriture
			_nourriture = nourriture;
		}
	}
	
	/**
	 * Calcul avec getTimeFactor
	 * @param newPop
	 * @throws TimeFactorInconnu
	 * @since 24 nov. 2011
	 */
	public void setPop(final int newPop) throws TimeFactorInconnu
	{		
		// Si la nourriture a changé, on doit changer le temps restant après le nextPop
		if (_pop != newPop)
		{
			// On modifie tous les temps de construction
			for (final Construction construction : _constructions)
			{
				construction.applyTimeFactor(Temps.getTimeFactor(_pop, newPop));
			}
			
			// On modifie la pop
			_pop = newPop;
			
			// ainsi que le nextPop
			_nextPop = getInitialNextPop();
		}
	}

//	/**
//	 * @param nourriture
//	 * @return facteur de modification des temps de construction (voir {@link #getTimeFactor(double)})
//	 */
//	public void nourriture(final int nourriture) {
//		
//		// Si la nourriture a changé, on doit changer le temps restant après le nextPop
//		if (_nourriture != nourriture)
//		{
//			// ancien nextPopInitial
//			final double oldInitialNextPop = getInitialNextPop();
//			
//			// On modifie la nourriture
//			_nourriture = nourriture;
//			
//			_nextPop = Temps.applyTimeFactor(oldInitialNextPop, getInitialNextPop(), _nextPop);
//		}
//	}
	
//	public void setPop(final int newPop) throws TimeFactorInconnu
//	{		
//		// Si la nourriture a changé, on doit changer le temps restant après le nextPop
//		if (_pop != newPop)
//		{
//			// On modifie la pop
//			_pop = newPop;
//			
//			// On modifie tous les temps de construction
//			for (final Construction construction : _constructions)
//			{
//				construction.recalculerTempsRestant();
//			}
//			
//			// ainsi que le nextPop
//			_nextPop = getInitialNextPop();
//		}
//	}

	public void add(final TypeUnite unite) {
		
		// Batiment
		if (unite instanceof TypeBatiment)
		{
			final TypeBatiment batiment = (TypeBatiment) unite;
			final int oldNb;
			if (!_batiments.containsKey(batiment))
			{
				oldNb = 0;
			}
			else
			{
				oldNb = _batiments.get(batiment);
			}
			_batiments.put(batiment, oldNb + 1);
		}
		else if (unite instanceof TypeVaisseau)
		{
			final TypeVaisseau typeVaisseau = (TypeVaisseau) unite;
			getEtatJoueur().addVaisseau(typeVaisseau, getIle());
		}
	}
	
	/**
	 * @return temps (initial) avant la prochaine pop 
	 * @since 9 nov. 2011
	 */
	private double getInitialNextPop() {
		
		final int nourriture = nourriture();
		final int pop = pop();
		
		if (nourriture == pop)
		{
			return Temps.INFINI;
		}
		else if (nourriture < pop)
		{
			// FIXME : si nourriture < pop => le temps restant sera la mort d'un habitant
			System.err.println("La mort d'habitant n'a pas encore été implémentée");
			return Temps.INFINI;
		}
		else
		{
			return (double) 10800 / (nourriture - pop);
		}
	}

	public int nourriture() {
		return _nourriture;
	}

	public int pop() {
		return _pop;
	}

	public Ile getIle() {
		return _entite;
	}

	public void execute(NouvelHabitant nouvelHabitant) throws TimeFactorInconnu {
		// On incrémente la pop
		incPop(1);
	}
	
	/**
	 * @return true si on peut continuer ou commencer les constructions
	 * @since 10 nov. 2011
	 */
	public boolean peutConstruire()
	{
		if (_constructions.isEmpty())
		{
			return false;
		}
		else
		{
			final Construction prchConst = _constructions.peek();
			if (prchConst.aEteCommencee())
			{
				return prchConst.getTempsRestant() != Temps.INFINI;
			}
			else
			{
				return getEtatJoueur().tempsPourCollecter(prchConst.getCout()) != Temps.INFINI;
			}
		}
	}
	
	@Override
	public void avancer(double temps) throws ConstructionImpossible, EvenementInconnuOublie {
		
		// Diminue le temps de construction
		construire(temps);
		
		// Diminue le temps pour la prochaine pop
		_nextPop = Temps.decrementer(_nextPop, temps);
	}

	/**
	 * Nombre de bâtiments, d'un certain type, prévus dans les constructions
	 * @param batiment bâtiment à compter
	 * @param constructionLimite construction servant de limite (exclue) dans le comptage
	 * @return
	 * @since 17 nov. 2011
	 */
	public int getNbPrevuAvant(TypeBatiment batiment, final Construction constructionLimite) {
		int nb = 0;
		
		// On compte le nombre de bâtiment prévus avant la construction passée en argument 
		for (final Construction construction : _constructions)
		{
			// Construction limite trouvée
			if (construction == constructionLimite)
			{
				break;
			}
			else if (batiment == construction.getUnite())
			{
				++nb;
			}
		}
		
		return nb;
	}
}
